﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Diagnostics;
using System.IO;

//统一的资源加载类，只有通过这个类加载的资源才能进行热更
//目前这个类没有做资源的卸载管理，如果比较需要管理的话，自己需要决定使用怎样的资源管理策略。
//继承MonoBehaviour是为了方便使用StartCorutinue，其实保持静态类更合适
public class ResourceManager : MonoBehaviour{
	//热更资源的路径，在Editor模式下有机会更改（方便调试），正常情况下就是对应平台下的持久化存储目录
	private string assetDataPath = null;
	public string AssetDataPath
	{
		get
		{ 
			if (assetDataPath == null) {
				assetDataPath = Application.persistentDataPath;
			}
			return assetDataPath;
		}
	}
	//默认bundle路径，对应于streamingDataPath,在热更路径中找不到的资源都会来这个路径下寻找，
	//所以可以把暂时不需要热更，但是以后有可能热更的资源打成bundle放在这个目录下,Editor模式下有机会修改此路径。
	private string streamingDataPath = null;
	public string StreamingDataPath{
		get
		{ 
			if (streamingDataPath == null) {
				streamingDataPath = Application.streamingAssetsPath;
			}
			return streamingDataPath;
		}
	}
	//在Editor中，资源目录，不要与Unity目录的命名冲突,该目录应该与BundleBuilder的资源目录一致
	private string ResourceRoot = "Assets/Res";
	//是否从Bundle中加载？该值只在Editor模式中有效,其它模式下直接按照true去处理
	public bool LoadFromBundle = true;
	//当前已经加载过的Bundle，避免重复加载
	private Dictionary<string,AssetBundle> bundleMap = new Dictionary<string,AssetBundle>();

	//单例部分
	public static ResourceManager Instance{ get; private set;}
	static ResourceManager(){
		GameObject obj = new GameObject ("ResourceManager");
		DontDestroyOnLoad (obj);
		Instance = obj.AddComponent<ResourceManager> ();
	}

	//核心函数
	//加载资源的统一方法，根据平台和设置的不同，会从不同的地方加载资源
	public void LoadAsset<T>(string path,Action<T> onResult) where T : UnityEngine.Object{
		#if UNITY_EDITOR
		if(!LoadFromBundle){
			LoadAssetFromEditor<T>(path,onResult);
		}else{
			LoadAssetFromBundle<T>(path,onResult);
		}
		#else
		LoadAssetFromBundle<T>(path,onResult);
		#endif
	}

	//不读取bundle,直接从Unity项目中的相关位置（应该为Res目录）中进行读取
	//这好像会导致同步加载数据，不过问题不大，就当是瞬间完成的异步加载就好了，异步加载数据时不应当对合适加载完成有任何的假定
	[Conditional("UNITY_EDITOR")]
	private void LoadAssetFromEditor<T>(string path,Action<T> onResult)where T : UnityEngine.Object{
		#if UNITY_EDITOR
		onResult(UnityEditor.AssetDatabase.LoadAssetAtPath<T> (Path.Combine(ResourceRoot,path)));
		#endif
	}

	//核心函数，各平台（包括Editor模式下），通用的加载资源方式，该加载会先尝试寻找persistentDataPath,如果找不到，再去
	//寻找streamingAssetsPath，如果最终还是找不到，则返回null
	private AssetBundleManifest bundleManifest = null; //当前最新的BundleManifest。
	private void LoadAssetFromBundle<T>(string path,Action<T> onResult)where T : UnityEngine.Object{
		//先找到元数据bundle
		if (bundleManifest == null) {
			AssetBundle asset = LoadBundleSync ("ResourceMap");
			if (asset == null) {
				UnityEngine.Debug.LogError ("can't fint resource map");
				return;
			}
			bundleManifest = asset.LoadAsset<AssetBundleManifest> ("AssetBundleManifest");
			if (bundleManifest == null) {
				UnityEngine.Debug.LogError ("can't load asset bundle manifest");
				return;
			}
		}
		var bundleName = path.Substring(0,path.LastIndexOf('/'));
		bundleName = bundleName.Replace ('/','.');
		bundleName = bundleName + ".bundle";
		//打包的bundle名称被Unity强制设为全小写
		bundleName = bundleName.ToLower ();
		var assetName = path.Substring (path.LastIndexOf ('/')+1);
		//从元数据bundle中找到这个bundle依赖的bundle
		//这里能不能处理递归的依赖关系？ 官方示例中没有去处理递归依赖的情况，那就当做这个函数能自己处理吧
		var dependencies = bundleManifest.GetAllDependencies(bundleName);
		foreach (var dependence in dependencies) {
			//为了省心，这里全部使用同步加载的方式，用异步加载去处理更好,但是需要等到依赖文件全部准备完毕后，才能去加载真正的asset
			LoadBundleSync(dependence);
		}
		StartCoroutine (LoadBundleAsync (bundleName, bundle => {
			StartCoroutine(LoadAssetAsync<T>(bundle,assetName,onResult));
		}));
	}

	//加载某个bundle,一定是先寻找AssetDataPath,找不到再找StreamingDataPath
	//这个函数设置为public,主要方便热更部分可以直接拿到Manifest的数据，理论上这里不应该是public
	//但是这个项目在设计时，没打算自己组织bundle的元数据，而是直接使用unity生成的minifest bundle,
	//所以才需要在热更时还需要读取本地的资源（这其实很不合理）
	public AssetBundle LoadBundleSync(string bundleName){
		AssetBundle assetBundle = null;
		bundleMap.TryGetValue (bundleName, out assetBundle);
		if (assetBundle != null) {
			return assetBundle;
		}
		//这里加载不出来会打印一个警告，但不是异常，所以不管，正好当做log了。实际项目中可以使用File.Exists判断一下文件是否存在
		assetBundle = AssetBundle.LoadFromFile (Path.Combine (AssetDataPath, bundleName));
		if (assetBundle == null) {
			assetBundle = AssetBundle.LoadFromFile (Path.Combine (StreamingDataPath, bundleName));
		}
		if (assetBundle != null) {
			assetBundle.name = bundleName;
			bundleMap[bundleName] = assetBundle;
		}
		return assetBundle;
	}

	//异步加载某一个名称的bundle
	private IEnumerator LoadBundleAsync(string bundleName,Action<AssetBundle> onResult){
		//先判断原来有没有加载过bundle
		AssetBundle assetBundle = null;
		bundleMap.TryGetValue (bundleName, out assetBundle);
		if (assetBundle != null) {
			onResult (assetBundle);
			yield break;
		}
		//尝试去加载，这里加载不出来会打印一个警告，但不是异常，所以不管，正好当做log了。实际项目中可以使用File.Exists判断一下文件是否存在
		AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync (Path.Combine (AssetDataPath, bundleName));
		yield return request;
		if (request.assetBundle != null) {
			request.assetBundle.name = bundleName;
			bundleMap [bundleName] = request.assetBundle;
			onResult (request.assetBundle);
			yield break;
		}
		//第一次加载失败，就去备用目录中去尝试加载
		request = AssetBundle.LoadFromFileAsync (Path.Combine (StreamingDataPath, bundleName));
		yield return request;
		if (request.assetBundle != null) {
			request.assetBundle.name = bundleName;
			bundleMap [bundleName] = request.assetBundle;
			onResult (request.assetBundle);
		} else {
			//加载失败，可能是该bundle无法加载到，也有可能是多个地方同时去异步加载同一个bundle,第一个成功加载后，其它协程就会加载失败
			//但是此时bundle可能是已经加载好的。这里就直接多进行一步检查，如果需要更高的效率（比如避免重复的异步加载操作），则需要自己去做一些额外的管理
			//维护一个AssetBundleCreateRequest的Map，然后如果发现存在正常进行的加载，就去使用原来的加载，是可以行的，但哪会让这里的代码看起来更复杂。
			bundleMap.TryGetValue (bundleName, out assetBundle);
			onResult (assetBundle);
		}
	}

	//异步加载asset,将协程变成回调
	private IEnumerator LoadAssetAsync<T>(AssetBundle bundle,string assetName,Action<T> onResult) where T : UnityEngine.Object{
		if (bundle == null) {
			onResult (null);
			yield break;
		}
		var request = bundle.LoadAssetAsync<T> (assetName);
		yield return request;
		onResult ((T)request.asset);
	}

	[Conditional("UNITY_EDITOR")]
	public void ChangeAssetPath(string path){
		assetDataPath = path;
	}
	[Conditional("UNITY_EDITOR")]
	public void ChangeStreamingDataPath(string path){
		streamingDataPath = path;
	}

	//手动清空bundle缓存，在切换场景时（Unity会卸载bundle）或者自己手动卸载相关bundle时需要调用，避免加载到空的bundle缓存
	public void ClearBundleCache(){
		bundleMap.Clear ();
	}
	//获取bundleList的方式，方便外部手动进行bundle操作
	public Dictionary<string,AssetBundle> GetBundleMap(){
		return bundleMap;
	}
	//重置bundleList,根据当前内存中的AssetBundle列表重置bundleList,在切换场景或者清空手动完成bundle的回收后，应该调用此处
	public void ResetBundleList(){
		bundleMap.Clear ();
		foreach (var bundle in Resources.FindObjectsOfTypeAll<AssetBundle> ()) {
			bundleMap.Add (bundle.name, bundle);
		}
	}

	//一些加载特定类型资源的辅助函数，其实就是内部调用LoadAsset
	//加载.txt文件对应的文件内容字符串
	public void LoadString(string path,Action<string> onResult){
		LoadAsset<TextAsset> (path, textasset => {
			onResult (textasset.text);
		});
	}
	//加载.bytes文件对应的二进制数据，所有Unity不认识的文件格式都可以改成.bytes格式，Unity会将它原封不动的打包，然后在加载时可以获取
	//该文件对应的二进制数据流。理论上，这可以让AssetBundle打包任意类型文件
	public void LoadBytes(string path,Action<byte[]> onResult){
		LoadAsset<TextAsset> (path, textasset => {
			onResult(textasset.bytes);	
		});
	}
	//加载一般.prefab文件，作为GameObject加载出来
	public void LoadGameObject(string path,Action<GameObject> onResult){
		LoadAsset<GameObject> (path, onResult);
	}
	//加载图片,作为Texture2D加载出来
	public void LoadTexture2D(string path,Action<Texture2D> onResult){
		LoadAsset<Texture2D> (path, onResult);
	}
	//加载图片，作为Sprite加载出来
	public void LoadSprite(string path,Action<Sprite> onResult){
		LoadAsset<Sprite> (path, onResult);
	}
	//加载音频，作为AudioClip加载出来
	public void LoadAudioClip(string path,Action<AudioClip> onResult){
		LoadAsset<AudioClip> (path, onResult);
	}

	//加载ScriptableObject的子类的对象，可以参考此处 http://www.cnblogs.com/hammerc/p/4829934.html
	public void LoadScriptObject<T>(string path,Action<T> onResult) where T : ScriptableObject{
		LoadAsset<T> (path, onResult);
	}
}
